﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.Collections;
using System.Xml;
using System.IO;
using System.IO.IsolatedStorage;
using log4net;

namespace Entangle
{
    class SymmetricKeyRing

    {
        
        private static Random r = new Random();
        private static readonly ILog log = LogManager.GetLogger(typeof(SymmetricKeyRing));

        private string fileName;
        public string Name { get; set; }
        public SymmetricKey this[string id]
        {
            get
            {
                if (keys.ContainsKey(id))
                    return keys[id];
                else
                    return null;
            }
        }

        private Dictionary<string,SymmetricKey> keys;

        public SymmetricKeyRing(string name, string pFileName)
        {
            Name = name;
            fileName = pFileName;
            keys = new Dictionary<string,SymmetricKey>();
            LoadKeys();
        }

        ~SymmetricKeyRing()
        {
            log.Debug("Finalizing SymmetricKeyRing: " + Name);
            SaveKeys();
        }

        private void LoadKeys()
        {

            log.Info("Loading symmetric keys...");

            log.Debug("Currently has: " + keys.Count + " keys.");
            log.Debug("Obtaining isolated storage.");
            IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly | IsolatedStorageScope.Domain, null, null);

            log.Debug("Checking for private key file.");
            string[] fileNames = isoStore.GetFileNames(fileName);
            if (fileNames.Length == 0)
            {
                log.Info("Symmetric keys file not found. No keys were loaded.");
                return;
            }
            else
            {
                log.Debug("Reading from file: " + fileName);
                using (StreamReader reader = new StreamReader(new IsolatedStorageFileStream(fileName, FileMode.Open, isoStore)))
                {
                    string symmKeyXML = reader.ReadToEnd();
                    reader.Close();

                    log.Debug("Loading XML");
                    XmlDocument xml = new XmlDocument();
                    try
                    {
                        xml.LoadXml(symmKeyXML);
                    }
                    catch (Exception e)
                    {
                        log.Error("Error loading symmetric keys from xml!", e);
                        throw;
                    }

                    XmlElement xeKeys = xml.DocumentElement;
                    XmlNodeList xnlKeys = xeKeys.SelectNodes("/Keys");

                    int loaded = 0, notLoaded = 0;
                    foreach (XmlNode xnKey in xnlKeys)
                    {
                        log.Debug("Loading symmetric key...");
                        if (LoadKeyFromXML(xnKey.OuterXml))
                        {
                            ++loaded;
                            log.Debug("Key loaded.");
                        }
                        else
                        {
                            ++notLoaded;
                            log.Debug("Key NOT loaded!");
                        }
                    }
                    log.Info(String.Format("Symmetric keys loaded({0}/{1}).", loaded, loaded + notLoaded));
                }
            }
        }

        public void SaveKeys()
        {
            log.Info("Saving symmetric keys...");
            
            log.Debug("Obtaining isolated storage.");
            IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly | IsolatedStorageScope.Domain, null, null);

            log.Debug("Writing to file: " + fileName);
            using (StreamWriter writer = new StreamWriter(new IsolatedStorageFileStream(fileName, FileMode.Create, isoStore)))
            {
                XmlDocument xml = new XmlDocument();
                XmlElement xeKeys = xml.CreateElement("Keys");

                foreach (SymmetricKey sk in keys.Values)
                {
                    xeKeys.InnerXml += sk.ToXMLString(false);
                }

                xml.AppendChild(xeKeys);

                writer.Write(xml.OuterXml);
                writer.Close();
            }

            log.Info("Symmetric keys saved.");
        }


        /// <summary>
        /// Creates new strong symmetric key and secures it.
        /// </summary>
        /// <returns>An ID of a key to reference it later</returns>
        public string CreateNewKey()
        {
            string id;
            do
            {
                // In case of duplicate key, practically impossible, but...
                byte[] bId = new byte[r.Next(16, 128)];
                r.NextBytes(bId);
                id = Convert.ToBase64String(bId);
            } while (keys.ContainsKey(id));

            RijndaelManaged aes = new RijndaelManaged();
            SymmetricKey key = new SymmetricKey(id, aes.Key, aes.IV);
            keys.Add(id, key);
            
            return id;
        }

        /// <summary>
        /// Loads AES key from XML string and decrypts it with private RSA key
        /// </summary>
        /// <param name="sXml">XML with key data</param>
        /// <returns>Result: if key was properly loaded</returns>
        public bool LoadKeyFromXML(string sXml)
        {
            XmlDocument xmld = new XmlDocument();
            try
            {
                xmld.LoadXml(sXml);
            }
            catch (Exception e)
            {
                log.Error("Error loading symmetric key from XML!", e);
                return false;
            }
            XmlElement xml = xmld.DocumentElement;
            XmlNode xnSymmetricKey = xml["SymmetricKey"];
            if (xnSymmetricKey == null)
            {
                log.Error("XML doesn't contain required 'SymmetricKey' element.");
                return false;
            }
            XmlAttribute xaUsedRSAHash = xnSymmetricKey.Attributes["UsedRSAHash"];
            string usedRSAHash = xaUsedRSAHash.InnerText;
            if (usedRSAHash == null || usedRSAHash == String.Empty)
            {
                log.Error("SymmetricKey element doesn't contain required 'UsedRSAHash' attribute.");
                return false;
            }

            if (usedRSAHash != Convert.ToBase64String(Cryptography.Instance.PublicKeyHash))
            {
                log.Error("Key is encrypted with unknown RSA assymetric key:"+usedRSAHash);
                return false;
            }

            XmlNode xnId = xnSymmetricKey["ID"];
            if (xnId == null)
            {
                log.Error("XML doesn't contain required 'ID' element.");
                return false;
            }
            string sId = xnId.InnerText;

            XmlNode xnIV = xnSymmetricKey["IV"];
            if (xnIV == null)
            {
                log.Error("XML doesn't contain required 'IV' element.");
                return false;
            }
            string sIV = xnIV.InnerText;

            XmlNode xnKey = xnSymmetricKey["Key"];
            if (xnKey == null)
            {
                log.Error("XML doesn't contain required 'Key' element.");
                return false;
            }
            string sKey = xnKey.InnerText;
            log.Debug("sId.length=" + sId.Length);
            string id = Cryptography.Instance.RSADecryptWithPrivateKey(sId);
            if (id == null)
            {
                log.Error("Cannot decrypt key id.");
                return false;
            }
            byte[] iv = Cryptography.Instance.RSADecryptWithPrivateKey(Convert.FromBase64String(sIV));
            if (iv == null)
            {
                log.Error("Cannot decrypt key IV.");
                return false;
            }
            byte[] key = Cryptography.Instance.RSADecryptWithPrivateKey(Convert.FromBase64String(sKey));
            if (key == null)
            {
                log.Error("Cannot decrypt key.");
                return false;
            }

            if (keys.ContainsKey(id))
            {
                log.Warn("Key: " + id + " is already loaded!");
                return true;
            }

            keys.Add(id, new SymmetricKey(id, key, iv));
            return true;
        }
    }
}
